{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Advanced Object Oriented Programming\n",
    "\n",
    "\n",
    "In the regular section on Object Oriented Programming (OOP) we covered:\n",
    "\n",
    "* Using the *class* keyword to define object classes\n",
    "* Creating class attributes\n",
    "* Creating class methods\n",
    "* Inheritance - where derived classes can inherit attributes and methods from a base class\n",
    "* Polymorphism - where different object classes that share the same method can be called from the same place\n",
    "* Special Methods for classes like `__init__`, `__str__`, `__len__` and `__del__`\n",
    "\n",
    "In this section we'll dive deeper into\n",
    "* Multiple Inheritance\n",
    "* The `self` keyword\n",
    "* Method Resolution Order (MRO)\n",
    "* Python's built-in `super()` function"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inheritance Revisited\n",
    "\n",
    "Recall that with Inheritance, one or more derived classes can inherit attributes and methods from a base class. This reduces duplication, and means that any changes made to the base class will automatically translate to derived classes. As a review:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fido says Woof!\n",
      "Isis says Meow!\n"
     ]
    }
   ],
   "source": [
    "class Animal:\n",
    "    def __init__(self, name):    # Constructor of the class\n",
    "        self.name = name\n",
    "\n",
    "    def speak(self):              # Abstract method, defined by convention only\n",
    "        raise NotImplementedError(\"Subclass must implement abstract method\")\n",
    "\n",
    "\n",
    "class Dog(Animal):\n",
    "    def speak(self):\n",
    "        return self.name+' says Woof!'\n",
    "    \n",
    "class Cat(Animal):\n",
    "    def speak(self):\n",
    "        return self.name+' says Meow!'\n",
    "    \n",
    "fido = Dog('Fido')\n",
    "isis = Cat('Isis')\n",
    "\n",
    "print(fido.speak())\n",
    "print(isis.speak())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, the derived classes did not need their own `__init__` methods because the base class `__init__` gets called automatically. However, if you do define an `__init__` in the derived class, this will override the base:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Animal:\n",
    "    def __init__(self,name,legs):\n",
    "        self.name = name\n",
    "        self.legs = legs\n",
    "\n",
    "class Bear(Animal):\n",
    "    def __init__(self,name,legs=4,hibernate='yes'):\n",
    "        self.name = name\n",
    "        self.legs = legs\n",
    "        self.hibernate = hibernate\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is inefficient - why inherit from Animal if we can't use its constructor? The answer is to call the Animal `__init__` inside our own `__init__`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Yogi\n",
      "4\n",
      "yes\n"
     ]
    }
   ],
   "source": [
    "class Animal:\n",
    "    def __init__(self,name,legs):\n",
    "        self.name = name\n",
    "        self.legs = legs\n",
    "\n",
    "class Bear(Animal):\n",
    "    def __init__(self,name,legs=4,hibernate='yes'):\n",
    "        Animal.__init__(self,name,legs)\n",
    "        self.hibernate = hibernate\n",
    "        \n",
    "yogi = Bear('Yogi')\n",
    "print(yogi.name)\n",
    "print(yogi.legs)\n",
    "print(yogi.hibernate)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multiple Inheritance\n",
    "\n",
    "Sometimes it makes sense for a derived class to inherit qualities from two or more base classes. Python allows for this with multiple inheritance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Car:\n",
    "    def __init__(self,wheels=4):\n",
    "        self.wheels = wheels\n",
    "        # We'll say that all cars, no matter their engine, have four wheels by default.\n",
    "\n",
    "class Gasoline(Car):\n",
    "    def __init__(self,engine='Gasoline',tank_cap=20):\n",
    "        Car.__init__(self)\n",
    "        self.engine = engine\n",
    "        self.tank_cap = tank_cap # represents fuel tank capacity in gallons\n",
    "        self.tank = 0\n",
    "        \n",
    "    def refuel(self):\n",
    "        self.tank = self.tank_cap\n",
    "        \n",
    "    \n",
    "class Electric(Car):\n",
    "    def __init__(self,engine='Electric',kWh_cap=60):\n",
    "        Car.__init__(self)\n",
    "        self.engine = engine\n",
    "        self.kWh_cap = kWh_cap # represents battery capacity in kilowatt-hours\n",
    "        self.kWh = 0\n",
    "    \n",
    "    def recharge(self):\n",
    "        self.kWh = self.kWh_cap"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So what happens if we have an object that shares properties of both Gasolines and Electrics? We can create a derived class that inherits from both!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "0\n"
     ]
    }
   ],
   "source": [
    "class Hybrid(Gasoline, Electric):\n",
    "    def __init__(self,engine='Hybrid',tank_cap=11,kWh_cap=5):\n",
    "        Gasoline.__init__(self,engine,tank_cap)\n",
    "        Electric.__init__(self,engine,kWh_cap)\n",
    "        \n",
    "        \n",
    "prius = Hybrid()\n",
    "print(prius.tank)\n",
    "print(prius.kWh)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "5\n"
     ]
    }
   ],
   "source": [
    "prius.recharge()\n",
    "print(prius.kWh)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why do we use `self`?\n",
    "\n",
    "We've seen the word \"self\" show up in almost every example. What's the deal? The answer is, Python uses `self` to find the right set of attributes and methods to apply to an object. When we say:\n",
    "\n",
    "    prius.recharge()\n",
    "\n",
    "What really happens is that Python first looks up the class belonging to `prius` (Hybrid), and then passes `prius` to the `Hybrid.recharge()` method.\n",
    "\n",
    "It's the same as running:\n",
    "\n",
    "    Hybrid.recharge(prius)\n",
    "    \n",
    "but shorter and more intuitive!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Method Resolution Order (MRO)\n",
    "Things get complicated when you have several base classes and levels of inheritance. This is resolved using Method Resolution Order - a formal plan that Python follows when running object methods.\n",
    "\n",
    "To illustrate, if classes B and C each derive from A, and class D derives from both B and C, which class is \"first in line\" when a method is called on D?<br>Consider the following:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    num = 4\n",
    "    \n",
    "class B(A):\n",
    "    pass\n",
    "\n",
    "class C(A):\n",
    "    num = 5\n",
    "    \n",
    "class D(B,C):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Schematically, the relationship looks like this:\n",
    "\n",
    "\n",
    "         A\n",
    "       num=4\n",
    "      /     \\\n",
    "     /       \\\n",
    "     B       C\n",
    "    pass   num=5\n",
    "     \\       /\n",
    "      \\     /\n",
    "         D\n",
    "        pass\n",
    "\n",
    "Here `num` is a class attribute belonging to all four classes. So what happens if we call `D.num`?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "D.num"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You would think that `D.num` would follow `B` up to `A` and return **4**. Instead, Python obeys the first method in the chain that *defines* num. The order followed is `[D, B, C, A, object]` where *object* is Python's base object class.\n",
    "\n",
    "In our example, the first class to define and/or override a previously defined `num` is `C`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `super()`\n",
    "\n",
    "Python's built-in `super()` function provides a shortcut for calling base classes, because it automatically follows Method Resolution Order.\n",
    "\n",
    "In its simplest form with single inheritance, `super()` can be used in place of the base class name :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyBaseClass:\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    \n",
    "class MyDerivedClass(MyBaseClass):\n",
    "    def __init__(self,x,y,z):\n",
    "        super().__init__(x,y)\n",
    "        self.z = z\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that we don't pass `self` to `super().__init__()` as `super()` handles this automatically.\n",
    "\n",
    "In a more dynamic form, with multiple inheritance like the \"diamond diagram\" shown above, `super()` can be used to properly manage method definitions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def truth(self):\n",
    "        return 'All numbers are even'\n",
    "    \n",
    "class B(A):\n",
    "    pass\n",
    "\n",
    "class C(A):\n",
    "    def truth(self):\n",
    "        return 'Some numbers are even'\n",
    "    \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'All numbers are even'"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class D(B,C):\n",
    "    def truth(self,num):\n",
    "        if num%2 == 0:\n",
    "            return A.truth(self)\n",
    "        else:\n",
    "            return super().truth()\n",
    "            \n",
    "d = D()\n",
    "d.truth(6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Some numbers are even'"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d.truth(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the above example, if we pass an even number to `d.truth()`, we'll believe the `A` version of `.truth()` and run with it. Otherwise, follow the MRO and return the more general case."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For more information on `super()` visit https://docs.python.org/3/library/functions.html#super<br>  and https://rhettinger.wordpress.com/2011/05/26/super-considered-super/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Great! Now you should have a much deeper understanding of Object Oriented Programming!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
